﻿/**
 * SlotDuino
 * 
 * Copyright (c) 2011/2012 Thiago Hummel Moreira, Ricardo Mizuuti, Rafael Mufato, Bruno Sakai, Andre Turina
 * 
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
 * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace SlotDuino.Sockets
{
    /// <summary>
    /// This class provides a higher level of abstraction 
    /// on tcp socket communication, with event oriented API
    /// </summary>
    public class TcpClient: IDisposable
    {
        public enum ConnectionStatus
        {
            NeverConnected,
            Connecting,
            Connected,
            Disconnecting,
            Disconnected,
            Error
        }

        private const int BUFFER_SIZE = 256;

        private byte[] _buffer;
        private Socket _socket;
        private ConnectionStatus _status;
        private DateTime _lastRecived;
        private string _lastError;

        private AsyncCallback _dataReceivedCallBack;
        private AsyncCallback _dataSentCallBack;

        public event ConnectionStatusChangedEventHandler ConnectionStatusChanged;
        public event DataReceivedEventHandler DataReceived;
        public event EventHandler DataSent;

        public ConnectionStatus Status { get { return _status; } }
        public DateTime LastRecived { get { return _lastRecived; } }
        public string LastError { get { return _lastError; } }

        public TcpClient()
        {
            _buffer = new byte[BUFFER_SIZE];
            _status = ConnectionStatus.NeverConnected;
            _lastError = null;
        }

        #region Public API
        public void Connect(IPAddress host, int port)
        {
            if (null == _socket)
                _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            else if (_socket.Connected)
                return;

            _dataReceivedCallBack = new AsyncCallback(OnDataReceived);
            _dataSentCallBack = new AsyncCallback(OnDataSent);

            _socket.BeginConnect(host, port, new AsyncCallback(OnConnectEnd), _socket);

            //Change connection status
            SetStatus(ConnectionStatus.Connecting);
        }

        public void Send(string msg)
        {
            Send(UTF8Encoding.UTF8.GetBytes(msg));
        }

        public void Send(byte[] msg)
        {
            if (null == _socket || !_socket.Connected)
                return;

            _socket.BeginSend(msg, 0, msg.Length, SocketFlags.None, _dataSentCallBack, _socket);
        }

        public void Disconnect()
        {
            if (null == _socket || !_socket.Connected)
                return;

            _socket.BeginDisconnect(true, new AsyncCallback(OnDisconnectEnd), _socket);

            //Change connection status
            SetStatus(ConnectionStatus.Disconnecting);
        }
        #endregion

        private void SetStatus(ConnectionStatus newStatus)
        {
            //Fire the ConnectionStatusChanged event
            ConnectionStatusChangedEventHandler handler = ConnectionStatusChanged;
            if (null != handler)
                handler(this, new ConnectionStatusChangedEventArgs(newStatus, _status));

            _status = newStatus;
        }

        private void OnConnectEnd(IAsyncResult ar)
        {
            try
            {
                //Retrieve the socket from the state object.
                Socket socket = (Socket)ar.AsyncState;

                //Complete the connection.
                socket.EndConnect(ar);

#if DEBUG
                Console.WriteLine("Socket connected to {0}", socket.RemoteEndPoint.ToString());
#endif

                //Change connection status
                SetStatus(ConnectionStatus.Connected);

                //After conected we want to start listening
                WaitForData(socket);
            }
            catch (Exception ex)
            {
#if DEBUG
                Console.WriteLine("OnConnectEnd: {0}", ex.Message);
#endif
                RaiseError(ex.Message);
            }
        }

        private void WaitForData(Socket socket)
        {
            //Begin receiving the data from the remote device.
            socket.BeginReceive(_buffer, 0, BUFFER_SIZE, 0, _dataReceivedCallBack, socket);
        }

        private void OnDataReceived(IAsyncResult ar)
        {
            try
            {
                //Retrieve the socket from the state object.
                Socket socket = (Socket)ar.AsyncState;

                //Read data from the remote device.
                int bytesRead = socket.EndReceive(ar);

                if (bytesRead > 0)
                {
#if DEBUG
                    Console.WriteLine("Read {0} bytes from server.", bytesRead);
#endif
                    //Fire the DataReceived event
                    DataReceivedEventHandler handler = DataReceived;
                    if (null != handler)
                    {
                        byte[] received = new byte[bytesRead];
                        Array.Copy(_buffer, received, bytesRead);
                        handler(this, new DataReceivedEventArgs(received));
                    }

                    _lastRecived = DateTime.Now;
                }

                //Put the program back to listening
                WaitForData(socket);
            }
            catch (Exception ex)
            {
#if DEBUG                
                Console.WriteLine("OnDataReceived: {0}", ex.Message);
#endif
                //RaiseError(ex.Message);
            }
        }

        private void OnDataSent(IAsyncResult ar)
        {
            try {
                //Retrieve the socket from the state object.
                Socket socket = (Socket)ar.AsyncState;

                //Complete sending the data to the remote device.
                int bytesSent = socket.EndSend(ar);
#if DEBUG
                Console.WriteLine("Sent {0} bytes to server.", bytesSent);
#endif
                //Fire the DataSent
                EventHandler handler = DataSent;
                if (null != handler)
                    handler(this, new EventArgs());
            }
            catch (Exception ex)
            {
#if DEBUG
                Console.WriteLine("OnDataSent: {0}", ex.Message);
#endif
                RaiseError(ex.Message);
            }

        }

        private void OnDisconnectEnd(IAsyncResult ar)
        {
            //Change connection status
            SetStatus(ConnectionStatus.Disconnected);
        }

        private void RaiseError(string msg)
        {
            _lastError = msg;
            SetStatus(ConnectionStatus.Error);
        }

        /// <summary>
        /// IDisposable implementation
        /// </summary>
        public void Dispose()
        {
            _socket.Close();
        }
    }
    
    #region Event support classes
    public class ConnectionStatusChangedEventArgs : EventArgs
    {
        public TcpClient.ConnectionStatus Status { get; internal set; }
        public TcpClient.ConnectionStatus OldStatus { get; internal set; }

        public ConnectionStatusChangedEventArgs(TcpClient.ConnectionStatus newStatus, TcpClient.ConnectionStatus oldStatus)
        {
            Status = newStatus;
            OldStatus = oldStatus;
        }
    }
    public delegate void ConnectionStatusChangedEventHandler(TcpClient sender, ConnectionStatusChangedEventArgs e);


    public class DataReceivedEventArgs : EventArgs
    {
        public byte[] DataReceived { get; internal set; }

        public DataReceivedEventArgs(byte[] dataReceived)
        {
            DataReceived = dataReceived;
        }
    }

    public delegate void DataReceivedEventHandler(TcpClient sender, DataReceivedEventArgs e);
    #endregion
}
